Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Remove ldap caching and move to the Zitadel v2 API #79

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

tlater-famedly
Copy link
Contributor

@tlater-famedly tlater-famedly commented Oct 30, 2024

Closes: #24
Closes: #53
Closes: #30

WIP because there are some things to clear up still. The tests will all fail here, but with some fudging of the sources almost all tests pass.

There are a few things that need to be discussed/covered:

Multi-source config/test suite

Previously, we wanted to support specifying multiple sources simultaneously, since that came somewhat for free. With the removal of the cache, however, only the UKT source uses email addresses to identify users, and in fact only it has a concept of "removed" users, which means that it fundamentally needs to be handled separately from the other sources. Trying to support multi-source definitions is much more tricky with this limitation, so I believe we should not do so.

This is the cause of most of the remaining failures; the test suite was written around the assumption that we can just dump all kinds of sources into one big config file, and for getting this over the line I've ignored also rewriting the test suite so far, instead testing by only having the respective source actively usable in code for the time being.

External user ID encoding

Since we universally use the external user IDs to uniquely identify users now, and we actually pull data from Zitadel instead of just pushing, we now need to re-ingest the user IDs we've written to the nickname field. Unfortunately, because of ldap3's odd behavior of silently converting byte-typed values to strings when they can be, but otherwise returning bytes, we need to base64-encode these IDs.

So far we've just encoded any values that were bytes, and left strings alone - alas, apparently you can end up with strings randomly being valid base64 values (e.g. starttls). This means that we do not have a way to distinguish between IDs that were encoded and IDs that were not - when we get unlucky, this can make it impossible to uniquely identify a user, since decoding stringly-typed IDs can sometimes result in valid values.

Fixing this in the current implementation is easy (simply prefix encoded strings with a prefix, or encode all values, since UTF-8 strings are also valid byte strings), but backwards compatibility is a problem, since users with encoded IDs already exist in production.

It's possible that there is no way around runs in which some users will be deleted and immediately re-created.

Zitadel version

Due to the various recent Zitadel bugs, we need to update the Zitadel version in this PR. I've currently gone with the latest (2.64.1), but maybe we want another version?


Remaining tasks:

@tlater-famedly tlater-famedly requested a review from a team as a code owner October 30, 2024 11:53
@emgrav
Copy link
Member

emgrav commented Oct 30, 2024

It's possible that there is no way around runs in which some users will be deleted and immediately re-created.

For UKE we'll need to make sure to test this with a dry run

user.set_idp_links(vec![IdpLink::new()
.with_user_id(imported_user.external_user_id.to_string())
.with_idp_id(self.zitadel_config.idp_id.clone())
// TODO: Figure out if this is the correct value; empty is not permitted
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be confirmed to work before we merge; we have no way to test if IDP logins actually work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I ended up reading the source code to figure out how Zitadel handles IDP IDs, I also figured out what the "name" means. I originally just wrote this comment:

This refers to the "username" that the search filter defined in the IDP config resolves to. This means that for our IDP links to work, that needs to be configured to resolve to the user's email address. See: https://github.com/zitadel/zitadel/blob/250f2344c8c2292ca9b861cdd12223d0b4719d43/internal/idp/providers/ldap/session.go#L230

That should make it into the readme, though.

@tlater-famedly
Copy link
Contributor Author

It's possible that there is no way around runs in which some users will be deleted and immediately re-created.

For UKE we'll need to make sure to test this with a dry run

The UKE source should be unaffected, because it doesn't need to parse external IDs from Zitadel. But yeah, probably prudent to do dry runs for the first sync after updating in general, this is a pretty big change.

emgrav
emgrav previously approved these changes Oct 31, 2024
src/lib.rs Show resolved Hide resolved
@emgrav
Copy link
Member

emgrav commented Oct 31, 2024

Remaining before this can be merged:

  1. Some tests need to be refactored to no longer combine multiple sources.
  2. We need to document the potential quirks of external user ID encoding, including instructions for infra on what to look for in a dry run to identify a problem before it happens.

@jannden
Copy link
Contributor

jannden commented Nov 3, 2024

There is a clash between CSV and LDAP at this point. When we import CSV, it wrongly deletes Zitadel users not present in the CSV (or rather users that it couldn't match).
A) Is that a desired behavior?
B) The old and new users are compared with their external_user_id which won't match if we mix CSV and LDAP, because they use different external_user_id. CSV uses email addresses for that and LDAP uses user_id attribute pulled from LDAP for that. Shall we add another field in CSV to explicitly add a user_id that has the potential to match LDAP user_id?

@tlater-famedly
Copy link
Contributor Author

There is a clash between CSV and LDAP at this point. When we import CSV, it wrongly deletes Zitadel users not present in the CSV (or rather users that it couldn't match).

A) Is that a desired behavior?

Ideal behavior would of course be that switching between LDAP/CSV is possible, but this is not a use case we actually currently have, so while not ideal, it's probbaly fine for this to happen.

B) The old and new users are compared with their external_user_id which won't match if we mix CSV and LDAP, because they use different external_user_id. CSV uses email addresses for that and LDAP uses user_id attribute pulled from LDAP for that. Shall we add another field in CSV to explicitly add a user_id that has the potential to match LDAP user_id?

Yes, this is already a planned feature, see #75. We should do that as a separate task, though, as mixing the sources will for now be unsupported.

We should probably make sure with product that this is indeed fine, however.

@jannden
Copy link
Contributor

jannden commented Nov 4, 2024

Okay, in that case, we can merge the updated tests from: #80

Copy link

codecov bot commented Nov 4, 2024

Codecov Report

Attention: Patch coverage is 92.01774% with 36 lines in your changes missing coverage. Please review.

Project coverage is 91.38%. Comparing base (483af5f) to head (7b1b501).

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/lib.rs 87.20% 16 Missing ⚠️
src/zitadel.rs 91.57% 15 Missing ⚠️
src/sources/csv.rs 75.00% 4 Missing ⚠️
src/main.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #79      +/-   ##
==========================================
+ Coverage   86.76%   91.38%   +4.62%     
==========================================
  Files           7        8       +1     
  Lines        1352     1277      -75     
==========================================
- Hits         1173     1167       -6     
+ Misses        179      110      -69     
Files with missing lines Coverage Δ
src/config.rs 97.76% <100.00%> (+1.45%) ⬆️
src/sources/ldap.rs 97.85% <100.00%> (+2.16%) ⬆️
src/sources/ukt.rs 86.20% <ø> (-0.53%) ⬇️
src/user.rs 100.00% <100.00%> (+10.29%) ⬆️
src/main.rs 0.00% <0.00%> (ø)
src/sources/csv.rs 93.28% <75.00%> (-0.10%) ⬇️
src/zitadel.rs 93.05% <91.57%> (+17.59%) ⬆️
src/lib.rs 87.20% <87.20%> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 483af5f...7b1b501. Read the comment docs.

emgrav
emgrav previously approved these changes Nov 4, 2024
jannden
jannden previously approved these changes Nov 4, 2024
@emgrav
Copy link
Member

emgrav commented Nov 5, 2024

When we merge this we should tag a new version and notify Niklas

@lukaslihotzki-f lukaslihotzki-f mentioned this pull request Nov 5, 2024
src/user.rs Outdated Show resolved Hide resolved
tests/e2e.rs Show resolved Hide resolved
src/user.rs Outdated
/// See
/// https://www.notion.so/famedly/Famedly-UUID-Specification-adc576f0f2d449bba2f6f13b2611738f
pub fn famedly_uuid(&self) -> String {
Uuid::new_v5(&FAMEDLY_NAMESPACE, self.external_user_id.as_bytes()).to_string()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer correct. We need to encode the raw byte value in the UUID, not the hex-encoded string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should write tests that explicitly confirm this, so we don't accidentally change it again.

src/zitadel.rs Outdated
};
if self.feature_flags.is_enabled(FeatureFlag::SsoLogin) {
user.set_idp_links(vec![IdpLink::new()
.with_user_id(imported_user.external_user_id.clone())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to use the broken base64-encoded scheme here for Zitadel to interpret the IDs "correctly".

We should probably also send an issue upstream about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a test to confirm this is correctly encoding IDs too.

@tlater-famedly tlater-famedly force-pushed the tlater/remove-cache branch 4 times, most recently from 3b8816b to 15dca27 Compare November 14, 2024 14:06

let user = zitadel.get_user_by_login_name("delete_me@famedly.de").await;
assert!(user.is_err_and(|error| matches!(error, ZitadelError::TonicResponseError(status) if status.code() == TonicErrorCode::NotFound)));
}

// Currently fails because CSV uses non-hex encoded IDs, need to think
// about how to fit this into the overall workflow
#[ignore]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not working yet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the CSV backend should simply also hex-encode any IDs, and we simply make this an assumption of all sources so that we can rely on this being correct all the time?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all IDs should go through the encoding

@@ -1,17 +1,17 @@
#![cfg(test)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add one more test, to assert that SSO linking actually works. I believe Zitadel is set up to allow this already, we just need to add a test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a new test test_e2e_sso_linking. Is that what you meant? Feel free to tweak it.

@@ -1,17 +1,17 @@
#![cfg(test)]
#![allow(clippy::expect_fun_call)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add this to the clippy config, and add it to our company-wide clippy config, if we don't like that lint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to create an issue out of this and leave it explicitly in the test file for this PR.

Copy link
Contributor

@jannden jannden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of things I noticed, meaning as points of discussion.

src/main.rs Outdated Show resolved Hide resolved
src/sources/csv.rs Outdated Show resolved Hide resolved
src/user.rs Outdated Show resolved Hide resolved
src/user.rs Show resolved Hide resolved
Copy link
Contributor

@jannden jannden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good for manual tests now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Get rid of "cache" concept Allow non-SSO deployments Switch to new API
3 participants